﻿#if PocketPC
using System.Security.Cryptography.PBKDF2.CF;
#endif

namespace System.Security.Cryptography.PBKDF2
{
    public static class PBKDF2Helper
    {
        public const char DefaultPackingSeperator = '-';

        private const int DefaultMinIterations = 4096;
        private const int DefaultMaxIterationsIncrement = 1024;
        private const int DefaultMinSaltLength = 32;
        private const int DefaultMaxSaltLengthIncrement = 32;
        private const int DefaultMinKeyLength = 32;
        private const int DefaultMaxKeyLengthIncrement = 32;

        public static readonly Random StaticRandomNumberGenerator = new Random();
        public static readonly RNGCryptoServiceProvider StaticRandomCryptoNumberGenerator = new RNGCryptoServiceProvider();

        public static string CreatePackedHash(string password)
        {
            return CreatePackedHash(
                password, DefaultPackingSeperator,
                DefaultMinIterations, DefaultMaxIterationsIncrement,
                DefaultMinSaltLength, DefaultMaxSaltLengthIncrement,
                DefaultMinKeyLength, DefaultMaxKeyLengthIncrement);
        }

        public static string CreatePackedHash(string password, out int iterations, out byte[] salt, out byte[] key)
        {
            return CreatePackedHash(
                password, DefaultPackingSeperator,
                DefaultMinIterations, DefaultMaxIterationsIncrement,
                DefaultMinSaltLength, DefaultMaxSaltLengthIncrement,
                DefaultMinKeyLength, DefaultMaxKeyLengthIncrement,
                out iterations, out salt, out key);
        }

        public static string CreatePackedHash(
            string password, char seperator,
            int minIterations, int maxIterationsIncrement,
            int minSaltLength, int maxSaltLengthIncrement,
            int minKeyLength, int maxKeyLengthIncrement)
        {
            int iterations = 0;
            byte[] salt = null, key = null;

            return CreatePackedHash(
                password, seperator,
                minIterations, maxIterationsIncrement,
                minSaltLength, maxSaltLengthIncrement,
                minKeyLength, maxKeyLengthIncrement,
                out iterations, out salt, out key);
        }

        public static string CreatePackedHash(
            string password, char seperator, 
            int minIterations, int maxIterationsIncrement,
            int minSaltLength, int maxSaltLengthIncrement,
            int minKeyLength, int maxKeyLengthIncrement,
            out int iterations, out byte[] salt, out byte[] key)
        {
            if (int.MaxValue - maxIterationsIncrement < minIterations)
            {
                throw new ArgumentOutOfRangeException("maxIterationsIncrement", "minIterations + maxIterationsIncrement must not overflow ");
            }

            if (int.MaxValue - maxSaltLengthIncrement < minSaltLength)
            {
                throw new ArgumentOutOfRangeException("maxSaltLengthIncrement", "minSaltLength + maxSaltLengthIncrement must not overflow ");
            }

            if (int.MaxValue - maxKeyLengthIncrement < minKeyLength)
            {
                throw new ArgumentOutOfRangeException("maxKeyLengthIncrement", "minKeyLength + maxKeyLengthIncrement must not overflow ");
            }

            string packed = null;

            iterations = Math.Max(1, StaticRandomNumberGenerator.Next(minIterations, minIterations + maxIterationsIncrement));
            var saltLength = Math.Max(1, StaticRandomNumberGenerator.Next(minSaltLength, minSaltLength + maxSaltLengthIncrement));
            var keyLength = Math.Max(1, StaticRandomNumberGenerator.Next(minKeyLength, minKeyLength + maxKeyLengthIncrement));

            salt = new byte[saltLength];
            StaticRandomCryptoNumberGenerator.GetBytes(salt);

#if NET35
            var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
#else
            using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
#endif
            {
                key = pbkdf2.GetBytes(keyLength);

                packed = GetPackedhash(seperator, iterations, salt, key);
            }

            return packed;
        }

        public static string GetPackedhash(char seperator, int iterations, byte[] salt, byte[] key)
        {
            var iterationsBytes = BitConverter.GetBytes(iterations);
            var iterationsByteLength = 4;
            while (iterationsByteLength >= 0 && iterationsBytes[iterationsByteLength - 1] == 0)
            {
                --iterationsByteLength;
            }

            return string.Format("{1}{0}{2}{0}{3}", seperator,
                Convert.ToBase64String(iterationsBytes, 0, iterationsByteLength),
                Convert.ToBase64String(salt),
                Convert.ToBase64String(key));
        }

        public static bool Validate(string packedHash, string password)
        {
            return Validate(packedHash, password, DefaultPackingSeperator);
        }

        public static bool Validate(string packedHash, string password, char seperator)
        {
            bool isValid = false;

            var parts = packedHash.Split(seperator);
            var iterationBytes = new byte[4];
            Convert.FromBase64String(parts[0]).CopyTo(iterationBytes, 0);
            var iterations = BitConverter.ToInt32(iterationBytes, 0);
            var salt = Convert.FromBase64String(parts[1]);
            var key = Convert.FromBase64String(parts[2]);

#if NET35
            var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations);
#else
            using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations))
#endif
            {
                var passwordKey = pbkdf2.GetBytes(key.Length);

                isValid = passwordKey.SlowEquals(key);
            }

            return isValid;
        }

        public static bool SlowEquals(this byte[] a, byte[] b)
        {
            uint diff = (uint)a.Length ^ (uint)b.Length;
            for (int i = 0; i < a.Length && i < b.Length; i++)
                diff |= (uint)(a[i] ^ b[i]);
            return diff == 0;
        }
    }
}
